﻿/*****************************************介绍*****************************************
 * 作者：Ywh
 * 创建时间：2023-12-28　10:33:57
 * 功能：Scroll View优化
 *      1、实现可以容纳任何数量级的对象而不卡顿
 *      注意：
 *          - padding 的left 和 right目前没有作用
 *          - 目前只支持纵向滚动的操作
 *          - Scrollbar Vertical 的滚动方向不支持改动，只支持向下滚动（Buttom To Top）
 *          - Item对象的锚点必须设置为：左上角，中心点也必须在左上角
 *          - Item对象的高度必须是固定的，目前不支持可变的高度
 *      使用方法：
 *          - 将该脚本必须挂载到【ScrollRect】组件所在的对象上
 *          - 获取到该脚本后，需要调用Init接口，并且注入一个实现了 IscrollViewCruxInfo 的对象
 *          - 如果需要刷新可视范围的内容，调用 Refresh接口即可
**************************************************************************************/

using System.Collections.Generic;
using System.Linq;
using UnityEngine.EventSystems;

namespace UnityEngine.UI.Optimize
{
    public class ScrollViewContentOptimize : UIBehaviour
    {
        [SerializeField]
        private Vector2 space;
        [SerializeField]
        private RectOffset padding;

        private ScrollRect scrollRect;
        private RectTransform rect;
        private float viewportWidth;
        private float viewportHeight;
        private float contentHeight;
        private IScrollViewCruxInfo cruxInfo;   // 必须信息
        private int curMinRow, curMaxRow;
        private int allColumn;                  // 总列数
        private int allRow;                     // 总行数
        private int realRow;                    // 实际行数
        private float startPosX;                // 起始位置x
        private Dictionary<int, List<GameObject>> allItems;

#if UNITY_EDITOR
        protected override void OnValidate()
        {
            if (UnityEditor.EditorApplication.isPlaying)
                Refresh();
        }
#endif

        // 计算列数
        private int CalcColumnNum()
        {
            int result = Mathf.FloorToInt(viewportWidth / cruxInfo.ItemWidth);
            while (result * cruxInfo.ItemWidth + (result - 1) * space.x > viewportWidth)
                result--;
            result = Mathf.Clamp(result, 1, int.MaxValue);
            return result;
        }

        // 计算行数
        private int CalcRowNum(int column)
        {
            int result = cruxInfo.ItemsCount / column;
            if (cruxInfo.ItemsCount % column > 0)
                result++;
            return result;
        }

        // 计算实际显示的行数
        private int CalcRealRowNum()
        {
            int result = Mathf.CeilToInt(viewportHeight / cruxInfo.ItemHeight);
            while (GetHeight() > viewportHeight)
                result--;

            if (viewportHeight - GetHeight() > 0)
                result += 2;
            return result;

            float GetHeight() => result * cruxInfo.ItemHeight + padding.top + padding.bottom + (result - 1) * space.y;
        }

        // 计算起始位置X的值（使用这一步则会居中显示Item）
        private float CalcStartPosX()
        {
            float allSpaceW = allColumn > 1 ? (allColumn - 1) * space.x : 0;
            float useWidth = allColumn * cruxInfo.ItemWidth + allSpaceW;
            float result = (viewportWidth - useWidth) * 0.5f;
            return result;
        }

        // 创建当前行的对象
        private void CreateCurRowItems(int curRow, bool isForward = true)
        {
            if (curRow >= allRow || curRow < 0) return;
            if (allItems.ContainsKey(curRow))
                DeleteCurRowItems(curRow);

            float posY = GetCurRowPosY(curRow);
            int startIndex = curRow * allColumn;
            var objs = new List<GameObject>(allColumn);
            allItems.Add(curRow, objs);
            //LogManager.Log($"添加第 {curRow} 行");

            for (int j = 0; j < allColumn; j++)
            {
                int curIndex = startIndex + j;
                if (curIndex < cruxInfo.ItemsCount)
                {
                    // 计算当前行对象的位置x
                    float posX = GetCurColumnPosX(j);
                    //LogManager.Log($"{j}列：{posX}");
                    var itemRect = cruxInfo.GetItemInstance();
                    itemRect.SetParent(scrollRect.content);
                    itemRect.localScale = Vector3.one;
                    itemRect.anchoredPosition = new Vector2(posX, posY);
                    if (isForward)
                        itemRect.SetAsLastSibling();
                    else
                        itemRect.SetSiblingIndex(j + 1);
                    cruxInfo.InstanceInitAfterHandle(itemRect.gameObject, curIndex);
                    objs.Add(itemRect.gameObject);
                }
            }
        }

        // 删除当前行的对象
        private void DeleteCurRowItems(int curRow)
        {
            if (allItems.TryGetValue(curRow, out var objs))
            {
                for (int i = 0; i < objs.Count; i++)
                {
                    int index = curRow * allColumn + i;
                    cruxInfo.DestoryInstance(objs[i], index);
                }
                allItems.Remove(curRow);
                //LogManager.Log($"删除第 {curRow} 行");
            }
        }

        // 获取当前列的位置X
        private float GetCurColumnPosX(int curColumn)
        {
            float spaceWidth = curColumn * space.x + padding.left;
            return curColumn * cruxInfo.ItemWidth + spaceWidth;
        }

        // 获取当前行的位置y
        private float GetCurRowPosY(int curRow)
        {
            return curRow * -cruxInfo.ItemHeight - curRow * space.y - padding.top;
        }

        // 计算当前多拽出去了多少行（也是最小显示的行标）
        private int CalcDragoutRowNum()
        {
            float contentY = scrollRect.content.anchoredPosition.y;
            float lineHeight = cruxInfo.ItemHeight + space.y;
            int curRow = Mathf.Clamp(Mathf.FloorToInt(contentY / lineHeight - padding.top / lineHeight), 0, int.MaxValue);
            return curRow;
        }

        private void FindObject()
        {
            if (scrollRect != null) return;
            scrollRect = GetComponent<ScrollRect>();
            rect = scrollRect.transform as RectTransform;
            scrollRect.onValueChanged.AddListener(ContentPosChangedHandler);
        }

        private void CaclCruxData()
        {
            if (rect == null) return;
            viewportWidth = rect.rect.width;
            viewportHeight = rect.rect.height;
            allColumn = CalcColumnNum();
            allRow = CalcRowNum(allColumn);
            contentHeight = allRow * cruxInfo.ItemHeight + (allRow - 1) * space.y + padding.top + padding.bottom; // 总高度
            realRow = CalcRealRowNum();
            curMinRow = CalcDragoutRowNum();
            curMaxRow = Mathf.Clamp(curMinRow + realRow, 0, allRow);
            startPosX = CalcStartPosX();
        }

        // 更新Content内容
        private void ContentPosChangedHandler(Vector2 pos)
        {
            int curRow = CalcDragoutRowNum();

            int dir = curRow - curMinRow;
            int needHandleCount = Mathf.Abs(dir);
            if (needHandleCount > 0)
            {
                if (needHandleCount < realRow)
                    for (int i = 0; i < needHandleCount; i++)
                    {
                        if (dir > 0)
                        {
                            curMaxRow = curMinRow + realRow;
                            CreateCurRowItems(curMaxRow++);
                            if (curMinRow >= allRow - realRow) continue;
                            DeleteCurRowItems(curMinRow++);
                        }
                        else if (dir < 0 && curMinRow > 0)
                        {
                            CreateCurRowItems(--curMinRow, false);
                            curMaxRow = curMinRow + realRow;
                            DeleteCurRowItems(curMaxRow);
                        }
                    }
                else
                {
                    //LogManager.Log($"操作太快，需要特殊处理{needHandleCount}次---{curRow}---{curMinRow}");
                    // 计算新的首行
                    curMinRow = Mathf.Clamp(dir > 0 ? curMinRow + needHandleCount : curMinRow - needHandleCount, 0, allRow - realRow);
                    // 计算新的末行
                    curMaxRow = Mathf.Clamp(curMinRow + realRow, realRow, allRow);

                    // 生成最新Items
                    CreateViewAllItems();
                }
            }
            //LogManager.Log($"curRow:{curRow}---min:{curMinRow}---max:{curMaxRow}---dir:{dir}");
        }

        /// <summary>初始化优化器，只执行一次</summary>
        /// <param name="info">关键信息类</param>
        public void Init(IScrollViewCruxInfo info)
        {
            cruxInfo = info;
            if (info == null) return;
            FindObject();
            if (allItems == null) allItems = new Dictionary<int, List<GameObject>>();
            Refresh();
            ContentMoveToTop();
        }

        /// <summary>
        /// 刷新优化器，该接口一般在容器数量发生变化时调用，会进行以下操作：
        /// <para>1、重新计算所有关键信息</para>
        /// <para>2、刷新显示对象</para>
        /// </summary>
        public void Refresh()
        {
            if (allItems == null) return;
            CaclCruxData();
            allItems.EnsureCapacity(realRow);
            scrollRect.content.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, contentHeight);   // 设置高度
            CreateViewAllItems();
        }

        /// <summary>刷新UI高度</summary>
        public void RefreshHeight()
        {
            CaclCruxData();
            scrollRect.content.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, contentHeight);   // 设置高度
        }

        // 生成可视区域的所有项
        public void CreateViewAllItems()
        {
            ClearAll();
            for (int i = curMinRow; i < curMaxRow; i++)
            {
                CreateCurRowItems(i);
            }
        }

        /// <summary>删除已经创建的所有内容</summary>
        public void ClearAll()
        {
            foreach (var key in allItems.Keys.ToArray())
                DeleteCurRowItems(key);
        }

        /// <summary>滚动到最开始</summary>
        public void ContentMoveToTop()
        {
            FindObject();
            scrollRect.verticalNormalizedPosition = 1;
        }

        /// <summary>滚动到末尾</summary>
        public void ContentMoveToButtom()
        {
            FindObject();
            scrollRect.verticalNormalizedPosition = 0;
        }

        /// <summary>获取Scroll View组件合适的宽度（用来提前设置宽度，主要在UI上使用）</summary>
        public float GetAppropriateWidth()
        {
            if (cruxInfo == null) return 0;
            int column = CalcColumnNum();
            float width = column * (cruxInfo.ItemWidth + space.x) + space.x;
            return width;
        }

        /// <summary>获取指定行的对象列表</summary>
        /// <param name="row">指定行</param>
        /// <returns>如果存在行则返回对应列表，否则返回一个空（长度为0）列表</returns>
        public List<GameObject> GetRowItems(int row)
        {
            if (!allItems.TryGetValue(row, out List<GameObject> items))
                return new List<GameObject>(0);
            return items;
        }

        /// <summary>根据索引来获取对应的Item</summary>
        /// <param name="index">Item的索引</param>
        /// <returns>如果不存在则返回Null，反之返回对应的Item对象</returns>
        public GameObject GetItemByIndex(int index)
        {
            if (index < CurItemMinIndex || index > CurItemMaxIndex)
                return null;

            int row = index / TotalColumn;
            int col = index % TotalColumn;

            if (allItems.TryGetValue(row, out var list))
            {
                if (list.Count > col)
                    return list[col];
            }
            return null;
        }

        public override string ToString()
        {
            return $"width:{viewportWidth}--height:{viewportHeight}--yPos:{scrollRect.content.anchoredPosition.y}--contentHeight:{contentHeight}\nallColumn:{allColumn}--allRow:{allRow}--realRow:{realRow}--curMinRow:{curMinRow}--curMaxRow:{curMaxRow}";
        }

        /// <summary>总列数</summary>
        public int TotalColumn => allColumn;

        /// <summary>总行数</summary>
        public int TotalRow => allRow;

        /// <summary>实际创建的行数</summary>
        public int RealRow => realRow;

        /// <summary>当前已创建对象的最小行索引</summary>
        public int CurMinRow => curMinRow;

        /// <summary>当前已创建对象的最大行索引</summary>
        public int CurMaxRow => curMaxRow - 1;

        /// <summary>当前已创建对象的最小索引</summary>
        public int CurItemMinIndex => curMinRow * allColumn;

        /// <summary>当前已创建对象的最大索引</summary>
        public int CurItemMaxIndex => Mathf.Clamp(CurMaxRow * allColumn + allColumn - 1, 0, cruxInfo.ItemsCount - 1);
    }
}